3.04. Справочник по эмодзи
Разработчику
Аналитику
Тестировщику
Архитектору
Инженеру
Справочник по эмодзи
📚 Часть 1: Основы стандарта, свойства и классификация
1. Что такое «эмодзи» с точки зрения стандарта Unicode
Слово emoji (絵文字) — японское заимствование: e («рисунок») + moji («знак, символ»). Однако в контексте IT эмодзи — не отдельный набор символов, а надмножество графем Unicode, обладающих специальными свойствами и обработкой.
Согласно Unicode Technical Standard #51 (UTS #51), версия 15.1 (2024), эмодзи определяются не просто как «картинки», а как:
«Символы, которые используются для выражения идей, эмоций, объектов или концепций посредством графического представления, и которые включены в официальную таблицу Emoji в рамках стандарта Unicode.»
Важно: не каждый пиктографический символ — эмодзи. Например, символы Dingbats (U+2700–U+27BF, Dingbats) формально не являются эмодзи, хотя визуально могут быть похожи (✔, ✘, ✏), если только они не включены в официальный список Emoji=Yes.
2. Основные свойства эмодзи в Unicode
Каждому символу Unicode может быть сопоставлен ряд свойств. Для эмодзи наиболее важны:
| Свойство | Тип | Возможные значения | Описание |
|---|---|---|---|
Emoji | Boolean | Yes / No | Является ли символ базовым эмодзи (см. Emoji=Yes в emoji-data.txt) |
Emoji_Presentation | Boolean | Yes / No | По умолчанию отображается как изображение (не как текстовый глиф) |
Emoji_Modifier | Boolean | Yes / No | Является модификатором (например, селекторы: U+1F3FB–U+1F3FF) |
Emoji_Modifier_Base | Boolean | Yes / No | К какому символу применимы модификаторы (например, 👋, 👩) |
Emoji_Component | Boolean | Yes / No | Является частью составной последовательности (включая ZWJ-sequences) |
Extended_Pictographic | Boolean | Yes / No | Расширенное пиктографическое поведение (используется для определения word boundaries, например, в ICU) |
⚠️ Важно:
Emoji=Yes≠ «отображается как картинка». Для этого необходимоEmoji_Presentation=Yes.- Например,
#(U+0023 NUMBER SIGN) имеетEmoji=Yes, ноEmoji_Presentation=No. Чтобы его показать как эмодзи (#️⃣), требуется секвенция:U+0023 U+FE0F U+20E3.
3. Типы эмодзи и их структура
3.1. Базовые эмодзи (Basic Emoji)
- Один кодпойнт, имеющий
Emoji=Yesи, как правило,Emoji_Presentation=Yes. - Примеры:
- 😀
U+1F600GRINNING FACE - 🐍
U+1F40DSNAKE - 🚀
U+1F680ROCKET
- 😀
3.2. Эмодзи с вариантом отображения (Variation Sequences)
- Базовый символ + Variation Selector-16 (
U+FE0F) — чтобы принудительно включить emoji-рендеринг. - Примеры:
©→©️(U+00A9 U+FE0F)#→#️⃣(U+0023 U+FE0F U+20E3 — keycap sequence)*→*️⃣(U+002A U+FE0F U+20E3)
🔍 Примечание:
U+FE0E(VS-15) — text presentation, т.е. отключает emoji-стиль. Например:❤︎(U+2764 U+FE0E) — сердце как текстовый символ.- Не все шрифты поддерживают VS-15/16 — поведение может варьироваться.
3.3. Keycap Sequences («клавиатурные эмодзи»)
Формат: [0-9#*] + U+FE0F + U+20E3 (COMBINING ENCLOSING KEYCAP).
| Эмодзи | Код | Примечание |
|---|---|---|
| 0️⃣ | U+0030 U+FE0F U+20E3 | |
| 1️⃣ | U+0031 U+FE0F U+20E3 | |
| … | ||
| #️⃣ | U+0023 U+FE0F U+20E3 | |
| *️⃣ | U+002A U+FE0F U+20E3 |
Не путать с цифрами в окружении: ① (U+2460) — это circled digit one, не эмодзи (Emoji=No).
3.4. Skin Tone Modifiers (Fitzpatrick Scale)
- Модификаторы от U+1F3FB до U+1F3FF (Light to Dark Skin Tone).
- Применяются только к символам с
Emoji_Modifier_Base=Yes. - Образуют графемный кластер:
[Base] + [Modifier]
| Модификатор | Название | Пример (с 👋) |
|---|---|---|
| U+1F3FB | Light Skin Tone | 👋🏻 |
| U+1F3FC | Medium-Light | 👋🏼 |
| U+1F3FD | Medium | 👋🏽 |
| U+1F3FE | Medium-Dark | 👋🏾 |
| U+1F3FF | Dark | 👋🏿 |
✅ Проверка совместимости:
person(U+1F9D1),woman(U+1F469),man(U+1F468),hand(U+1F590, U+270B),nail polish(U+1F485) и др.- Не все базовые эмодзи поддерживают модификаторы (например, 🐶 — нет).
3.5. ZWJ-sequences (Zero Width Joiner)
- Составные эмодзи, объединённые через
U+200D(ZWJ). - Порядок важен:
woman + ZWJ + heart + ZWJ + woman≠woman + ZWJ + woman + ZWJ + heart.
Классические типы ZWJ-sequences:
| Тип | Пример | Последовательность (hex) |
|---|---|---|
| Семья | 👨👩👧👦 | 1F468 200D 1F469 200D 1F467 200D 1F466 |
| Профессия | 👩⚕️ | 1F469 200D 2695 FE0F |
| Пара с сердцем | 👩❤️💋👨 | 1F469 200D 2764 FE0F 200D 1F48B 200D 1F468 |
| Люди с атрибутами | 🧑🦰 | 1F9D1 200D 1F9B0 (red hair) |
⚠️ Особенности:
- Внутри ZWJ-sequences могут быть вложенные variation selectors (например,
⚕→⚕️=2695 FE0F).- Если рендерер не поддерживает полную последовательность — она «распадается» на отдельные эмодзи (fallback).
- Для однозначной идентификации используется Emoji Sequence ID из
emoji-sequences.txt.
3.6. Флаги (Flags)
Два вида:
-
Государственные флаги — через Regional Indicator Symbols (U+1F1E6–U+1F1FF, A–Z).
- 🇷🇺 =
U+1F1F7 (R) + U+1F1FA (U) - Поддерживаются только для кодов ISO 3166-1 alpha-2, и только если платформа их реализует.
- Например,
XX(U+1F1FD U+1F1FD) — не отображается как флаг («non-flag»).
- 🇷🇺 =
-
Поднятый флаг (🏴, 🏁, 🎌) — базовые эмодзи, могут комбинироваться с цветами через ZWJ:
- 🏴 =
U+1F3F4 [E1100-E1163] U+E007F(subdivision flag) - Реализовано через Tag Sequence:
🏴+U+E0067(g) +U+E0062(b) +U+E0065(e) +U+E006E(n) +U+E0067(g) +U+E007F(cancel tag)
- 🏴 =
📌 Subdivision flags (подфлаги регионов) — крайне фрагментарны в поддержке (работают только в iOS/macOS, частично в Android ≥12).
4. Официальная классификация эмодзи (Unicode 15.1)
Все эмодзи разбиты на 11 групп и ~150 подгрупп. Примеры:
| Группа | Подгруппы | Кол-во эмодзи (Unicode 15.1) |
|---|---|---|
| Smileys & Emotion | face-smiling, face-affection, face-tongue, hand-fingers-open, heart | 183 |
| People & Body | person, person-gesture, person-activity, person-fantasy, family, person-symbol | 422 |
| Animals & Nature | animal-mammal, animal-bird, plant-flower | 150 |
| Food & Drink | food-fruit, food-vegetable, drink | 123 |
| Travel & Places | place-map, place-religious, building, hotel, time | 218 |
| Activities | sport, game, music, party, award-medal | 94 |
| Objects | clothing, sound, tool, science, medical | 228 |
| Symbols | transport-sign, warning, arrow, religion, zodiac | 208 |
| Flags | flag, subdivision-flag | 270 |
| Component | skin-tone, hair-style, gender | 12 |
| Итого базовых + sequences | — | 3782 |
📊 По состоянию на Unicode 15.1:
- Базовых символов с
Emoji=Yes: 1424- Полных последовательностей (включая ZWJ, keycap, flags): 3782
- Уникальных графем (emoji grapheme clusters): ≈ 4200+ (из-за combinatorics skin/gender/ZWJ)
📚 Часть 2: Данные стандарта — emoji-data.txt, emoji-sequences.txt, emoji-variation-sequences.txt
Эта часть посвящена технической документации Unicode, используемой разработчиками для корректной обработки эмодзи: как идентифицировать, валидировать, нормализовать и классифицировать эмодзи в коде. Акцент — на форматах .txt, предоставляемых официально на unicode.org.
1. Основные файлы данных (Unicode 15.1)
| Файл | URL (15.1) | Назначение |
|---|---|---|
emoji-data.txt | emoji-data.txt | Свойства Emoji, Emoji_Presentation, Emoji_Modifier, Emoji_Modifier_Base, Emoji_Component, Extended_Pictographic для отдельных кодпойнтов |
emoji-sequences.txt | emoji-sequences.txt | Все официальные составные последовательности: ZWJ, family, skin tone, flags, subdivision flags |
emoji-variation-sequences.txt | emoji-variation-sequences.txt | Variation sequences: [base] + FE0E/FE0F, keycaps (*#0-9 + FE0F + 20E3) |
emoji-test.txt | emoji-test.txt | Тестовые данные для валидации рендерера: status, group/subgroup, CLDR short names, sequences для всех уровней поддержки (fully-qualified, minimally-qualified, unqualified) |
⚠️ Эти файлы — единственный нормативный источник для определения, является ли последовательность «официальным эмодзи». Любая ad-hoc комбинация (например,
🐶+🔥) — не эмодзи, даже если визуально склеивается в некоторых шрифтах.
2. Формат emoji-data.txt
Каждая строка — либо диапазон (XXXX..YYYY), либо одиночный кодпойнт (XXXX), затем ; и список свойств.
Примеры строк:
1F600 ; Emoji # E0.6 😀 grinning face
1F601 ; Emoji # E0.6 😁 grinning face with smiling eyes
1F602 ; Emoji # E0.6 😂 face with tears of joy
231A..231B ; Emoji # E0.6 ⌚ ⌛ watch, hourglass
231A ; Emoji_Presentation # E0.6 ⌚ watch
231B ; Emoji_Presentation # E0.6 ⌛ hourglass
1F466..1F469 ; Emoji # E0.6 👦 👧 👨 👩 boy, girl, man, woman
1F466 ; Emoji_Modifier_Base # E0.6 👦 boy
1F467 ; Emoji_Modifier_Base # E0.6 👧 girl
1F468 ; Emoji_Modifier_Base # E0.6 👨 man
1F469 ; Emoji_Modifier_Base # E0.6 👩 woman
1F3FB..1F3FF ; Emoji_Modifier # E1.0 🏻 🏼 🏽 🏾 🏿 emoji modifier Fitzpatrick type-1-2..6
1F9B0..1F9B3 ; Emoji_Component # E11.0 🦰 🦱 🦲 🦳 red hair, curly hair, bald, white hair
Структура поля:
<codepoint(s)> ; <property> # <emoji_version> <sample> <name>
<emoji_version>— версия Unicode, в которой свойство было введено (не путать с Unicode Core version!). Например,E15.1= Unicode 15.1 Emoji.<sample>— примерный глиф (может не совпадать с вашим рендером).<name>— официальное имя по стандарту.
📌 Примечание:
Свойства независимы. Один кодпойнт может иметь несколько:
U+1F469(👩):
Emoji = YesEmoji_Presentation = YesEmoji_Modifier_Base = YesEmoji_Component = No
3. Формат emoji-variation-sequences.txt
Определяет variation sequences, влияющие на визуальное представление.
Формат:
<base> FE0F ; <name> # <status>
Примеры:
0023 FE0F ; number sign # fully-qualified
002A FE0F ; asterisk # fully-qualified
0030 FE0F ; digit zero # fully-qualified
...
2695 FE0F ; staff of Aesculapius # fully-qualified (⚕️)
2696 FE0F ; scale # fully-qualified (⚖️)
2708 FE0F ; airplane # fully-qualified (✈️)
🔍 Особые случаи:
- Keycap sequences — записаны отдельно, т.к. требуют 3 кодпойнта:
0030 FE0F 20E3 ; keycap: 0 # fully-qualified
0031 FE0F 20E3 ; keycap: 1 # fully-qualified
...
0023 FE0F 20E3 ; keycap: # # fully-qualified
002A FE0F 20E3 ; keycap: * # fully-qualified- Статус
minimally-qualifiedозначает, чтоFE0Fможно опустить, но тогда рендеринг может быть текстовым (например,⚕vs⚕️).
4. Формат emoji-sequences.txt
Содержит все официальные составные эмодзи. Четыре типа:
| Тип | Пример | Формат записи |
|---|---|---|
| Basic Emoji + Skin Tone | 👋🏽 | 1F44B 1F3FD |
| ZWJ Sequence | 👩💻 | 1F469 200D 1F4BB |
| Flag | 🇷🇺 | 1F1F7 1F1FA |
| Subdivision Flag | 🏴 | 1F3F4 E0067 E0062 E0065 E006E E0067 E007F |
Структура строки:
<sequence> ; <type> ; <CLDR name> # <status>
Примеры:
1F469 200D 1F3A4 ; ZWJ ; woman singer # fully-qualified
1F468 200D 1F469 200D 1F466 ; ZWJ ; man, woman, boy # fully-qualified (семья)
1F468 1F3FB 200D 1F469 1F3FE 200D 1F466 1F3FF ; ZWJ ; man, woman, boy: light, medium-dark, dark skin tones # fully-qualified
1F1F7 1F1FA ; RGI_Emoji_Flag_Sequence ; flag: Russian Federation # fully-qualified
1F3F4 E0067 E0062 E0065 E006E E0067 E007F ; RGI_Emoji_Tag_Sequence ; flag: England # fully-qualified
1F466 1F3FB ; Basic_Emoji ; boy: light skin tone # fully-qualified
📌 Ключевые термины в
status:
fully-qualified— полная, каноническая форма (все модификаторы и VS присутствуют).minimally-qualified— можно опуститьFE0Fили skin tone (но тогда fallback).unqualified— неофициальная, но допустимая (например,👩❤️👨безFE0Fна ❤ —2764вместо2764 FE0F).component— только часть (например,1F3FBсам по себе — не эмодзи, а модификатор).
📎 RGI = Recommended for General Interchange — последовательности, которые должны поддерживаться всеми платформами, претендующими на совместимость.
5. Формат emoji-test.txt — для тестирования рендереров
Этот файл — «истина в последней инстанции» для разработчика, создающего валидатор или рендерер.
Структура:
# group: Smileys & Emotion
# subgroup: face-smiling
1F600 ; fully-qualified # 😀 E0.6 grinning face
1F603 ; fully-qualified # 😃 E0.6 grinning face with big eyes
1F604 ; fully-qualified # 😄 E0.6 grinning face with smiling eyes
1F601 ; fully-qualified # 😁 E0.6 beaming face with smiling eyes
1F606 ; fully-qualified # 😆 E0.6 grinning squinting face
# subgroup: face-affection
1F60D ; fully-qualified # 😍 E0.6 smiling face with heart-eyes
1F970 ; fully-qualified # 🥰 E11.0 smiling face with 3 hearts
💡 Используется для:
- Генерации тестовых наборов (например, в ICU, emoji-picker библиотеках)
- Проверки покрытия поддержки эмодзи в вашем стеке
- Автоматического извлечения коротких имён (
grinning face) для i18n (CLDR)
6. Как программно работать с данными
6.1. Парсинг диапазонов
Пример на Python (без внешних зависимостей):
def parse_emoji_data_line(line: str) -> tuple[list[int], set[str], str]:
if not line or line.startswith('#'):
return [], set(), ''
parts = line.split('#')[0].strip().split(';')
if len(parts) < 2:
return [], set(), ''
codepoints_str = parts[0].strip()
props = {p.strip() for p in parts[1].split()}
# Разбор диапазона или одиночного кодпойнта
if '..' in codepoints_str:
start, end = codepoints_str.split('..')
cps = list(range(int(start, 16), int(end, 16) + 1))
else:
cps = [int(codepoints_str, 16)]
return cps, props, line
6.2. Валидация эмодзи-кластера
Псевдокод алгоритма (на основе UTS #51, Annex A):
function isEmojiGraphemeCluster(sequence: List[int]) → bool:
# 1. Разбить на Unicode grapheme clusters (по правилам UAX #29)
clusters = segmentIntoGraphemeClusters(sequence)
for cluster in clusters:
if cluster matches any of:
- [Emoji=Yes & Emoji_Presentation=Yes]
- [Emoji=Yes] + [FE0F]
- [Emoji_Modifier_Base=Yes] + [Emoji_Modifier=Yes]
- [Basic_Emoji] + [ZWJ] + [Emoji_Component=Yes]+
- [Regional_Indicator]+ (длина 2)
- [Tag_Base] + [Tag_Spec]* + [Cancel_Tag]
then continue
else:
return false
return true
📚 Источник: UTS #51, Annex A: Emoji Grapheme Cluster Definition
6.3. Нормализация в fully-qualified форму
- Добавить
FE0Fк символам сEmoji=Yes && Emoji_Presentation=Yes && Variation_Selector absent - Для skin-tone: если база поддерживает, и модификатор в диапазоне, сохранить как есть
- Для ZWJ: проверить по
emoji-sequences.txt, есть ли exact match; если нет — fallback на отдельные эмодзи
7. Статистика по Unicode 15.1 (цифры из файлов)
| Категория | Количество |
|---|---|
Кодпойнтов с Emoji=Yes | 1 424 |
Из них с Emoji_Presentation=Yes | 1 251 |
Emoji_Modifier_Base=Yes | 112 |
Emoji_Modifier=Yes | 5 (U+1F3FB–U+1F3FF) |
Emoji_Component=Yes | 17 (включая hair, gender, object parts) |
| ZWJ-sequences (все) | 1 237 |
| Из них fully-qualified | 952 |
| Flags (RGI) | 270 (256 стран + 14 субъектов) |
| Keycap sequences | 12 (0-9, *, #) |
🔢 Общее число графем (emoji grapheme clusters) = сумма всех fully-qualified последовательностей + базовые = 3 782
(Данные:emoji-test.txt: строкfully-qualified= 3 782 на 2024-09-05)
📚 Часть 3: Рендеринг и платформенные различия
Как одни и те же коды отображаются в разных системах — технические причины, fallback-логика, доступность
Эта часть описывает, почему один и тот же эмодзи может выглядеть по-разному на разных устройствах и в каких случаях это приводит к неоднозначности или ошибкам. Акцент — на инженерные аспекты: шрифты, рендереры, версии ОС, поведение fallback, размеры и метрики, поддержка accessibility.
1. Основные факторы различий в отображении
| Фактор | Описание | Пример последствий |
|---|---|---|
| Шрифт | Эмодзи не встроены в Unicode — они реализуются через шрифты (обычно цветные: COLR/CPAL, SVG-in-OpenType, CBDT/CBLC, sbix). | На Linux без fonts-noto-color-emoji — квадраты или чёрно-белые символы. |
| Версия шрифта | Производители выпускают обновления ежегодно (вместе с новой версией Unicode Emoji). | 🫠 U+1FAE0 (melting face) появился в iOS 15.4 (март 2022), но на Windows 10 до 22H2 — fallback. |
| Рендерер ОС | Windows (DirectWrite), macOS/iOS (Core Text), Android (FreeType + Skia), Linux (Pango + HarfBuzz) по-разному обрабатывают ZWJ-sequences и variation selectors. | 👩❤️💋👨 на Windows 10 (до 21H2) → 👩❤💋👨 (распадается), на iOS 17 — единый кластер. |
| Версия Unicode в библиотеке ICU | ICU (International Components for Unicode) управляет segmenter'ами, collation, date formatting, и участвует в определении emoji boundaries. | Intl.Segmenter в Node.js 18+ использует ICU ≥70; ранее — мог игнорировать skin tone как отдельный кластер. |
| Поддержка графемных кластеров | Некоторые старые редакторы/движки (например, Vim до 8.2) разбивают ZWJ-sequences на отдельные символы при подсчёте длины. | 👩💻.length в JS = 5 (кодпойнтов), но 1 (графем). |
2. Основные шрифты-носители эмодзи
| Платформа | Шрифт | Технология цвета | Год первого релиза | Поддержка Unicode 15.1 |
|---|---|---|---|---|
| Apple | Apple Color Emoji | sbix (bitmap) | 2008 (iPhone OS) | Полная (iOS 17.4+, macOS 14.4+) |
| Noto Color Emoji | CBDT/CBLC (bitmap) + COLRv1 (начиная с 2022) | 2013 | Полная (Android 14 QPR2+, Chrome ≥113) | |
| Microsoft | Segoe UI Emoji | COLR/CPAL (OpenType 1.9) | 2015 (Windows 8.1) | Частичная: отсутствуют subdivision flags, некоторые ZWJ-sequences (например, 🫱🏻🫲🏿 — handshake с разным skin tone) |
| Linux (Google) | Noto Color Emoji (open-source) | CBDT, позже COLRv1 | 2016 | Полная (при установке v2.035+, apt install fonts-noto-color-emoji) |
| Linux (Mozilla) | Twemoji Mozilla | SVG-in-OpenType (устаревший) | — | Неполная, deprecated |
| Twitter/X | Twemoji | SVG (встраивается в DOM как <img>) | 2014 | Полная, но с собственным дизайном (отличается от системного) |
📌 Колонка «Поддержка Unicode 15.1» означает:
- Все 3 782 fully-qualified sequences отображаются как единый кластер
- Skin tone, gender, hair style применяются корректно
- Subdivision flags (🏴) поддерживаются
⚠️ Windows — главный «отстающий»:
- Windows 10 (21H2): поддерживает только до Unicode 13.1 (2020)
- Windows 11 22H2: Unicode 14.0
- Windows 11 23H2: Unicode 15.0
- Windows 11 24H2 (ожидается 2024 Q4): Unicode 15.1
3. Fallback-логика при отсутствии поддержки
Когда рендерер не распознаёт последовательность, применяется стандартная стратегия постсимвольного fallback (UTS #51, §2.6):
- Попытка отобразить как единый кластер (например,
👩💻) - Если неизвестна ZWJ-sequence → разбить по ZWJ:
👩+💻 - Если
💻не имеетEmoji_Presentation=Yesи нетFE0F→ отобразить как текст (чёрно-белый глиф или системный символ) - Если кодпойнт вообще неизвестен → замена на U+FFFD () или прямоугольник □
Пример: 🧑🦰➡️♂️ (U+1F9D1 U+200D U+1F9B0 U+200D U+27A1 U+FE0F U+200D U+2642 U+FE0F)
— person, red hair, arrow, male
| Платформа | Результат (визуально) | Причина |
|---|---|---|
| iOS 17.4 | 🧑🦰➡️♂️ (единый кластер) | Полная поддержка |
| Android 13 | 🧑🦰➡️♂️ (4 части) | ZWJ после hair style не распознаётся |
| Windows 10 21H2 | 🧑🦰➡️♂ (распад, FE0F проигнорирован) | Отсутствие Emoji_Presentation для ➡ и ♂ без VS |
| Chrome 100 + Noto | 🧑🦰➡️♂️ | COLRv1 + актуальный Noto |
4. Размеры и метрики эмодзи
Эмодзи — не моноширинные, и их размер зависит от:
- Шрифта
- Уровня масштабирования системы (DPI scaling)
- Контекста (внутри
<span>,<button>,<textarea>)
Типичные размеры (в px, при 16pt, 96 DPI):
| Эмодзи | Apple | Microsoft | Примечание | |
|---|---|---|---|---|
😀 U+1F600 | 24×24 | 24×24 | 20×20 | Microsoft — компактнее |
🧑🚀 U+1F9D1 200D 1F680 | 28×28 | 28×28 | 24×24 | ZWJ-sequences часто шире |
| 🏴 | 24×18 | — | — | Subdivision flags — прямоугольные (флаги летают на ветру) |
0️⃣ U+0030 FE0F 20E3 | 20×26 | 20×26 | 18×24 | Вертикально вытянуты (keycaps) |
📏 Важно для вёрстки:
line-heightможет «прыгать», если в строке есть эмодзи разной высоты- В
textarea/input— курсор может «застревать» внутри ZWJ-кластера- В CSS:
font-size: 1em→ эмодзи масштабируется как текст, но не как1em × 1emsquare, если не заданоaspect-ratio: 1/1
Рекомендация для кросс-платформенной верстки:
.emoji-safe {
font-family: "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", "Twemoji Mozilla", sans-serif;
font-size: 1em;
line-height: 1.2; /* предотвращает переполнение */
display: inline-block; /* для контроля размера */
vertical-align: text-bottom; /* выравнивание по базовой линии */
}
5. Доступность (Accessibility)
Эмодзи — не замена тексту. Для пользователей скринридеров критична корректная озвучка.
Как это работает:
| Технология | Механизм |
|---|---|
| Apple VoiceOver | Использует CLDR short names из emoji-test.txt (например, «grinning face»). Поддерживает skin tone: «woman: medium skin tone». |
| Google TalkBack | Аналогично, но иногда упрощает: «couple with heart» вместо «woman, heart, man». |
| NVDA + Windows | Зависит от ICU и системного шрифта. До Windows 11 23H2 — часто «unknown symbol» для новых эмодзи. |
| JAWS | Требует обновления словаря; по умолчанию — «emoji» или код («U+1F600»). |
Рекомендации для разработчиков:
✅ Делайте:
- Добавляйте
aria-labelпри использовании эмодзи как иконки:<span aria-label="Ошибка: проверьте ввод" role="img">❌</span> - Используйте
<img alt="grinning face">для критичных случаев (например, в email) - Избегайте эмодзи в
aria-hidden="true"контенте
❌ Не делайте:
aria-label="😀"— скринридер произнесёт «U+1F600» или «grinning face», но это избыточно- Замену текста на эмодзи в интерфейсе («Отправить ✉️» → лучше «Отправить письмо ✉️»)
📊 Исследование WebAIM (2024):
68% пользователей скринридеров сообщают, что «эмодзи мешают пониманию», если не сопровождаются текстом.
6. Анимация и интерактивность
Некоторые платформы поддерживают анимированные эмодзи:
| Эмодзи | Платформа | Поведение |
|---|---|---|
😂 U+1F602 | iOS 17+, macOS 14+ | При long-press в Messages — слёзы падают |
🎉 U+1F389 | Telegram Desktop | Анимированная конфетти-частица (SVG + CSS) |
❤️ U+2764 FE0F | WhatsApp Web | Пульсация при реакции |
🛠 Технически:
- В мобильных ОС — встроенная анимация через Core Animation / Android Drawable
- В вебе — только кастомная реализация (Twemoji + CSS
@keyframes)- Стандарт Unicode не регулирует анимацию — это эксклюзив платформы
7. Практические таблицы совместимости
7.1. Поддержка ключевых новых эмодзи (Unicode 15.0–15.1)
| Эмодзи | Имя | Код | iOS | Android | Windows | Linux (Noto) | Замечания |
|---|---|---|---|---|---|---|---|
| 🫠 | melting face | 1FAE0 | 15.4+ | 13+ | 11 23H2+ | 2.030+ | На Windows 10 — □ |
| 🫨 | shaking face | 1FAE8 | 16.0+ | 14+ | 11 24H2 (ожид.) | 2.035+ | — |
| 🫰 | hand with index finger and thumb crossed | 1FAF0 | 16.4+ | 14 QPR2 | — | 2.035+ | В Windows — fallback на ✌️ |
| 🫱🏻🫲🏿 | handshake: light & dark skin | 1FAF1 1F3FB 200D 1FAF2 1F3FF | 16.4+ | 14 QPR2 | — | 2.035+ | На Windows — 👋🏻🤝🏿 (3 кластера) |
| 🪩 | mirror ball | 1FAA9 | 16.0+ | 14+ | 11 23H2+ | 2.033+ | — |
7.2. Проблемные ZWJ-sequences (частые точки отказа)
| Последовательность | Ожидаемое | Windows 11 22H2 | Android 12 | iOS 15.0 |
|---|---|---|---|---|
👩❤️💋👨 | couple kiss (W-W-M) | 👩❤💋👨 | 👩❤️💋👨 | 👩❤️💋👨 |
🧑🤝🧑 | handshake (neutral) | 🤝 | 🤝 | 🤝 |
🧑🤝🧑🏻 | handshake + skin | 🤝🏻 | 🤝 (игнорирует skin) | 🤝 (игнорирует skin) |
🏴 | flag: England | □□□□□□□ | □□□□□□□ | 🏴 |
🔍 Примечание:
- Skin tone в handshake (
🧑🤝🧑🏻) — технически invalid sequence поemoji-sequences.txt(требуется оба участника иметь skin tone:🧑🏻🤝🧑🏿).- Поэтому fallback корректен — стандарт не гарантирует отображение неканонических вариантов.
📚 Часть 4: Эмодзи в инфраструктуре
Хранение, обработка, поиск и совместимость в СУБД, API, JSON, файловых системах
Эта часть посвящена практическим аспектам эксплуатации эмодзи в IT-системах: как избежать потери данных, ошибок кодировки, некорректного поиска и проблем с миграцией. Акцент — на конкретные решения для MySQL, PostgreSQL, SQLite, JSON, UTF-8, а также на нормализацию и колляции.
1. Кодировка и представление в UTF-8
Эмодзи — это не «спецсимволы», а полноценные Unicode-кодпойнты. Однако их размер в UTF-8 варьируется:
| Тип | Пример | Кодпойнт | UTF-8 (байты) | Примечание |
|---|---|---|---|---|
| Базовый (BMP) | © | U+00A9 | C2 A9 (2) | Не эмодзи без VS-16 |
| Базовый (Astral) | 😀 | U+1F600 | F0 9F 98 80 (4) | Все эмодзи ≥ U+10000 — 4 байта |
| Skin tone | 👋🏽 | U+1F44B U+1F3FD | F0 9F 91 8B F0 9F 8F BD (8) | 2 × 4 байта |
| ZWJ-sequence | 👩💻 | U+1F469 U+200D U+1F4BB | F0 9F 91 A9 E2 80 8D F0 9F 92 BB (11) | 4 + 3 + 4 = 11 байт |
| Subdivision flag | 🏴 | 7 кодпойнтов | 28 байт | F0 9F 8F B4 F3 A0 81 A7 ... |
⚠️ Критичные ограничения:
- MySQL 5.5–5.7 (utf8): поддерживает только 3 байта → эмодзи обрезаются или заменяются на
???.- MySQL 8.0+ (utf8mb4): 4 байта — работает.
- PostgreSQL (UTF8): всегда 4 байта — корректно.
- SQLite (UTF-8): корректно, но
LENGTH()считает байты, а не символы.
2. СУБД: сравнительная таблица поддержки
| СУБД | Версия min | Кодировка | COLLATION | LENGTH() | SUBSTRING() | FULLTEXT поиск | Замечания |
|---|---|---|---|---|---|---|---|
| MySQL | 5.5.3+ | utf8mb4 | utf8mb4_unicode_520_ci (Unicode 5.2) utf8mb4_0900_ai_ci (Unicode 9.0) | байты | по байтам (если не CHAR) | Нет для эмодзи (до 8.0.19) | SET NAMES utf8mb4 обязателен. utf8 — ловушка. |
| PostgreSQL | 9.1+ | UTF8 | en_US.utf8, C.UTF-8, und-x-icu (с ICU) | символы (code points) | по code points | Да (через pg_trgm + unaccent) | CHAR_LENGTH() = grapheme ≠ code points. |
| SQLite | 3.0+ | UTF-8 | Нет (binary или NOCASE) | байты | по байтам | Нет | Используйте length(unicode_string) в Python/JS, а не SQL-LENGTH(). |
| SQL Server | 2012+ | UTF-8 (с 2019), ранее NVARCHAR (UTF-16) | Latin1_General_100_CI_AS_SC_UTF8 | символы (UTF-16 code units) | по code units | Частично | SC (Supplementary Characters) обязателен. |
📌 Ключевые термины:
SC(Supplementary Characters) — поддержка surrogate pairs (UTF-16) или astral plane (UTF-8)AI(Accent Insensitive),CI(Case Insensitive) — не влияют на эмодзи (они не имеют регистра/ударения)ICUcollation — используетIntl.Segmenter-подобную логику (PostgreSQL ≥12)
3. Практические проблемы и решения
3.1. Обрезка эмодзи при SUBSTRING
Проблема:
-- MySQL, utf8mb4
SELECT SUBSTRING('👋🏽', 1, 4); -- возвращает первые 4 *байта*: F0 9F 91 8B → 👋 (без skin tone)
Но 👋🏽 = 8 байт. Обрезка на 4 байтах — некорректный UTF-8, и клиент может показать .
Решение:
- В MySQL 8.0+:
SELECT SUBSTRING('👋🏽' USING utf8mb4); -- не помогает, всё равно по байтам
SELECT LEFT('👋🏽', 1); -- возвращает 👋 (первый кодпойнт), не кластер - В приложении: используйте grapheme-aware функции:
- Python:
import regex; regex.findall(r'\X', s) - JS:
[...s]— нет! ИспользуйтеIntl.Segmenter:const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const segments = [...segmenter.segment('👋🏽')];
segments[0].segment; // '👋🏽'
- Python:
3.2. Поиск эмодзи
Проблема:
-- MySQL
SELECT * FROM comments WHERE text LIKE '%😀%'; -- работает, если кодировка utf8mb4
SELECT * FROM comments WHERE text LIKE '%👩💻%'; -- может не найти, если в БД хранится без FE0F
Решения:
| Метод | PostgreSQL | MySQL 8.0+ | Примечание |
|---|---|---|---|
| Прямой поиск | WHERE text LIKE '%😀%' | То же | Требует точного совпадения последовательности |
| Поиск по базе | WHERE text ~* '\x{1F469}\x{200D}\x{1F4BB}' | REGEXP '👨💻' | Регулярки поддерживают Unicode escapes |
| Поиск игнорируя skin tone | Нет в SQL. Требует нормализации в приложении | — | См. ниже: «Нормализация» |
3.3. Индексация и сортировка
- MySQL:
utf8mb4_0900_ai_ci— сортирует эмодзи по code point (😀 < 😁 < 😂), но не по смыслу. - PostgreSQL с ICU:
CREATE COLLATION "emoji" (provider = icu, locale = 'und-u-kr-emoji');— сортировка по порядку вemoji-test.txt(группы → подгруппы). - Практика: не сортируйте по полям с эмодзи. Используйте отдельное
sort_order INTдля UI.
4. Нормализация эмодзи (NFC/NFD и beyond)
Unicode нормализация (NFC, NFD) не влияет на эмодзи, так как они не имеют combining marks в обычном смысле. Однако для эмодзи существуют свои правила:
4.1. Canonical Equivalence для эмодзи
| Неканоническая форма | Каноническая (fully-qualified) | Комментарий |
|---|---|---|
U+2764 (❤) | U+2764 U+FE0F (❤️) | Добавление VS-16 |
U+1F469 U+200D U+2764 U+200D U+1F468 | U+1F469 U+200D U+2764 U+FE0F U+200D U+1F468 | Добавление VS-16 к ❤ |
U+1F466 (👦) | U+1F466 U+1F3FB (👦🏻) — нет! | Skin tone — не нормализация, а семантическое расширение |
📜 Стандарт: UTS #51, §2.5: Emoji Normalization
4.2. Алгоритм нормализации в коде
Псевдокод (на основе emoji-variation-sequences.txt):
def normalize_emoji(s: str) -> str:
# 1. Найти все кластеры (grapheme segmentation)
clusters = segment_into_grapheme_clusters(s)
result = []
for cluster in clusters:
# 2. Проверить, есть ли exact match в emoji-sequences.txt (fully-qualified)
if cluster in fully_qualified_sequences:
result.append(cluster)
# 3. Иначе — попробовать добавить FE0F к базовым Emoji_Presentation=Yes
elif is_basic_emoji_without_vs(cluster):
base = cluster[0]
if has_property(base, 'Emoji_Presentation'):
result.append(codepoint_to_str(base) + '\uFE0F')
else:
result.append(cluster)
else:
result.append(cluster)
return ''.join(result)
🔍 Реализации:
- Python:
emoji—emoji.demojize(),emoji.emojize(),emoji.replace_emoji()- JavaScript:
emoji-regex+grapheme-splitter- C#:
EmojiData(библиотека с embedded emoji-data.txt)
5. JSON и эмодзи
5.1. Сериализация
- JSON RFC 8259 требует UTF-8, UTF-16 или UTF-32. Эмодзи допустимы без экранирования.
- Пример корректного JSON:
{
"message": "Привет, 🌍! Ты набрал 100 🪙.",
"reactions": ["👍", "❤️", "🚀"]
}
5.2. Проблемы
| Проблема | Причина | Решение |
|---|---|---|
UnicodeEncodeError в Python 2 | json.dumps() по умолчанию в ASCII | json.dumps(obj, ensure_ascii=False) |
Invalid \uXXXX escape в парсере | Эмодзи сохранены как \uD83D\uDE00 (UTF-16 surrogate) | Используйте UTF-8 end-to-end. Избегайте JSON_UNESCAPED_UNICODE в PHP без проверки кодировки. |
| Многострочные ZWJ в minified JSON | \u200D после минификации может слиться | Не минифицируйте JSON как строку — используйте AST-ориентированные инструменты (например, jq --compact-output) |
5.3. Размеры
| Строка | Длина (символы) | Размер UTF-8 (байт) | JSON-сериализация (без ensure_ascii=False) |
|---|---|---|---|
"😀" | 1 | 4 | "\\ud83d\\ude00" → 14 байт |
"😀" с ensure_ascii=False | — | — | "😀" → 6 байт (", 4 байта, ") |
"👩💻" | 5 (code points) | 11 | "\ud83d\udc69\u200d\ud83d\udcbb" → 38 байт |
📊 Вывод: всегда используйте
ensure_ascii=False(Python),JSON_UNESCAPED_UNICODE(PHP ≥5.4),JSON.stringify()в Node.js (по умолчанию UTF-8).
6. Файловые системы и ОС
| ОС / ФС | Поддержка имён файлов с эмодзи | Замечания |
|---|---|---|
| Windows NTFS | ✅ | Но: в CMD/PowerShell до 5.1 — отображение как . Требуется chcp 65001. |
| macOS APFS/HFS+ | ✅ | Используется NFD-нормализация — é = e + ◌́, но эмодзи не затрагиваются. |
| Linux ext4/xfs | ✅ | Зависит от локали терминала (LC_CTYPE=en_US.UTF-8). |
| ZIP-архивы | ⚠️ | Стандарт ZIP не указывает кодировку. Используйте флаг 0x0800 (EFS — UTF-8) или Info-ZIP convention. |
🛠 Проверка в Linux:
touch "🚀_launch.txt"
ls | hexdump -C # убедитесь, что F0 9F 9A 80 присутствует
7. Миграции и backward compatibility
Сценарий: переход с MySQL 5.7 (utf8) на 8.0 (utf8mb4).
Риски:
- Существующие эмодзи в
utf8— уже повреждены (???или обрезаны). ALTER TABLE CONVERT TO CHARACTER SET utf8mb4не восстановит данные.
Алгоритм безопасной миграции:
- Проверьте целостность:
SELECT * FROM comments WHERE HEX(text) LIKE '%EFBFBD%'; -- U+FFFD = - Сделайте дамп в UTF-8 (не в
utf8MySQL!):mysqldump --default-character-set=utf8mb4 --hex-blob db table > dump.sql - Импортируйте в новую БД с
utf8mb4иutf8mb4_0900_ai_ci. - Протестируйте на sample-данных с:
😀,👩💻,👋🏽,0️⃣,🏴
📌 Для PostgreSQL:
pg_dump --encoding=UTF8 --no-owner --no-privileges- Перед восстановлением:
CREATE DATABASE db ENCODING 'UTF8' LC_COLLATE 'C.UTF-8';
📚 Часть 5: Безопасность и уязвимости
Эмодзи как вектор атак: XSS, homograph-спуфинг, фишинг, спам, модерация
Эта часть посвящена практическим рискам, связанным с использованием эмодзи в веб- и мобильных приложениях, а также методам их предотвращения. Акцент — на воспроизводимые exploit-сценарии, механизмы срабатывания и проверенные контрмеры.
1. XSS и инъекции через эмодзи
Эмодзи не являются исполнимым кодом, но могут участвовать в обходе фильтров.
1.1. Обфускация payload через эмодзи в текстовом контексте
Сценарий:
Фильтр разрешает «только буквы, цифры и эмодзи», но не нормализует последовательности.
Exploit:
<!-- Ввод пользователя: -->
<button title="Click me 👁️🗨️">Submit</button>
<!-- Если backend некорректно парсит ZWJ: -->
<button title="Click me 👁️🗨️ onerror=alert(1)">Submit</button>
Как это работает:
👁️🗨️=U+1F441 U+FE0F U+200D U+1F5E8 U+FE0F- Некоторые парсеры (особенно на основе регулярных выражений) могут разбить ZWJ-sequence и вставить атрибут между
👁️и🗨️:<button title="Click me 👁️" onerror="alert(1)" 🗨️>Submit</button>
Уязвимые стеки:
- Устаревшие версии
DOMPurify(<2.4.0) безUSE_PROFILES: { svg: true } - Кастомные WAF-правила, использующие
\p{Emoji}без проверки на ZWJ integrity innerHTMLв браузерах с неполной поддержкой ZWJ (IE11, старый Edge)
Контрмеры:
✅ Валидируйте графемные кластеры, а не code points:
// Проверка: вся строка состоит из эмодзи-графем + разрешённых символов
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const isValid = [...segmenter.segment(input)].every(seg =>
seg.isEmoji || /[a-zA-Z0-9\s.,!?]/.test(seg.segment)
);
✅ Используйте DOMPurify.sanitize(input, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }) — он корректно обрабатывает ZWJ.
1.2. Эмодзи в URL и SSRF
Сценарий:
Пользователь вводит URL с эмодзи: https://api.example.com/fetch?url=https://тест.рф/🚀
Проблема:
- URL-парсеры могут декодировать
🚀какU+1F680, но HTTP-клиент не экранирует его вGET /🚀 HTTP/1.1 - Сервер может интерпретировать
🚀как часть пути и передать вcurl/HttpClientбез percent-encoding.
Риск:
- Если backend использует
exec('curl ' + url), возможна command injection через$(id)в домене (но не через эмодзи напрямую). - Основная угроза — некорректный логгинг:
🚀в логах может нарушать парсинг (например, Logstash падает при не-UTF8).
Контрмеры:
✅ Всегда percent-encode non-ASCII в URL:
encodeURI('https://тест.рф/🚀') // → 'https://xn--e1aybc.xn--p1ai/%F0%9F%9A%80'
✅ Валидируйте URL через new URL(input) — он выбросит, если последовательность недекодируема.
2. Homograph-атаки и визуальный спуфинг
Эмодзи могут визуально имитировать интерфейсные элементы.
2.1. Подмена статусов и иконок
| Эмодзи | Визуальный эффект | Риск |
|---|---|---|
🔒➡️🔓 | «Замок → разблокировано» | Фишинг: «Ваш аккаунт разблокирован! Нажмите 🔓» |
✅➡️❌ | Смена статуса | Подделка уведомлений: «Доставка ❌» (на самом деле ✅) |
👁️🗨️ | «Глаз в речевом пузыре» | Имитация «просмотрено» в мессенджерах |
0️⃣ vs O | Цифра 0 vs латинская O | Подделка серийных номеров, лицензий |
Доказательство концепции (Telegram):
Сообщение:
«Ваша сессия 🔒. Перейдите по ссылке для подтверждения: [ссылка] 🔓»
→ Пользователь думает, что сессия заблокирована, хотя на деле — защищена.
Контрмеры:
✅ Запретите эмодзи в критичных полях: email, логин, номер договора.
✅ В UI используйте SVG-иконки вместо эмодзи для статусов.
✅ Добавьте aria-label к эмодзи:
<span aria-label="Статус: защищено" role="img">🔒</span>
2.2. Emoji Domain Names (Punycode + эмодзи)
Хотя IDN не разрешает эмодзи напрямую, возможна комбинация:
xn--e1aybc.xn--p1ai(тест.рф) + эмодзи в пути/фрагменте- Но: браузеры блокируют эмодзи в домене (Chrome, Firefox, Safari).
Проверка:
new URL('https://🚀.example.com') // → TypeError: Invalid URL
Исключение:
- Старые версии браузеров (Safari ≤12) допускали
https://xn--🚀/— уязвимость закрыта в 2019.
3. Фишинг и социальная инженерия
3.1. Поддельные аватарки
Сценарий:
Атакующий использует 👩💼➡️👨💼 в имени профиля:
«Поддержка 👩💼➡️👨💼 (смена менеджера)»
→ Пользователь видит «женщина-менеджер стала мужчиной-менеджером» и доверяет.
Реальные кейсы:
- Telegram-каналы с
⚠️Официально⚠️в названии - Discord-серверы с
✅ Verifiedчерез эмодзи
Контрмеры:
✅ В UI обрезайте отображаемое имя до 20 символов графем, а не code points.
✅ Для верифицированных аккаунтов — отдельный badge, не эмодзи.
3.2. Эмодзи в SMS и USSD
Проблема:
- USSD-коды (
*100#) не поддерживают эмодзи. - Но:
*100#️⃣(keycap sequence) может отправиться как*100#+U+FE0F U+20E3→ оператор отбрасывает «лишнее», и запрос проходит.
Риск:
- Если SMS-шлюз не фильтрует эмодзи, возможна отправка
*100#️⃣💸→ интерпретируется как*100#+ текст (но USSD игнорирует пост-хэш-часть).
Контрмеры:
✅ В SMS-валидаторе удаляйте всё после #:
ussd = re.sub(r'#.*$', '#', user_input)
4. Спам и контент-модерация
4.1. Обход спам-фильтров
Техники:
- Вставка эмодзи между буквами:
V🩸I🩸A🩸G🩸R🩸A - Замена букв на визуально похожие эмодзи:
👁️вместоI,🅰️вместоA
Эффективность:
- Простые regex-фильтры (
/viagra/i) не срабатывают. - ML-модели (например, TensorFlow Serving с BERT) требуют fine-tuning на эмодзи-датасетах.
Датасеты для обучения:
- Emoji-Attacker (UC Irvine, 2023)
- Собирайте логи:
SELECT text FROM spam_reports WHERE text ~* '\p{Emoji}';
4.2. Модерация через API
| Сервис | Поддержка эмодзи | Примечание |
|---|---|---|
| Google Perspective API | ✅ | Оценивает toxicity даже в 🖕➡️👍 как high |
| Azure Content Moderator | ✅ | Классифицирует 🔞 как adult content |
| Amazon Rekognition Text | ✅ | Распознаёт 💊 как drug-related |
| Локальные решения (VADER, TextBlob) | ❌ | Игнорируют эмодзи без адаптации |
Рекомендация:
- Для локальной модерации используйте
emoji(Python) + словарь:toxic_emojis = {'🖕', '🤬', '💀', '💊', '🔞'}
if any(e in toxic_emojis for e in emoji.distinct_emoji_list(text)):
flag_as_toxic()
5. Приватность и отслеживание
5.1. Emoji Fingerprinting
Механизм:
- Браузер отображает эмодзи через шрифт → можно измерить ширину/высоту через
<canvas>:const ctx = canvas.getContext('2d');
ctx.font = '32px "Segoe UI Emoji", "Apple Color Emoji"';
const width = ctx.measureText('😀').width;
// width = 36 на Windows, 42 на macOS → fingerprint
Защита:
✅ В Tor Browser и Firefox с privacy.resistFingerprinting=true — фиксированные размеры эмодзи.
✅ Для веб-приложений — не используйте measureText() для эмодзи.
6. Таблица уязвимостей и контрмер
| Угроза | CWE | Уровень риска | Контрмера |
|---|---|---|---|
| XSS через ZWJ-разрыв | CWE-79 | Medium | Графемная сегментация, DOMPurify ≥2.4.0 |
| Homograph-спуфинг | CWE-297 | High | Запрет эмодзи в логинах/URL, aria-label |
| Фишинг через статусы | CWE-353 | Medium | SVG-иконки вместо эмодзи в UI |
| Обход спам-фильтров | CWE-116 | Low | ML-модели + emoji-словари |
| Emoji fingerprinting | CWE-200 | Low | Отказ от measureText() для эмодзи |
📌 CWE — Common Weakness Enumeration.
Риск оценен по методике CVSS 3.1:
- High: прямое влияние на конфиденциальность/целостность (подделка статуса → финансовые потери)
- Medium: требует user interaction (нажать на фишинговую ссылку)
- Low: сбор метаданных без прямого ущерба